use core::{
    fmt::{self, Alignment},
    ops::{Add, AddAssign, Div, Mul, Sub, SubAssign},
};
use num::{cast::AsPrimitive, NumCast};

const ONE_MHZ: u64 = 1_000_000;
const ONE_GHZ: u64 = ONE_MHZ * 1000;

pub trait ToFrequency {
    fn hz(self) -> Frequency;
    fn mhz(self) -> Frequency;
    fn ghz(self) -> Frequency;
}

#[repr(transparent)]
#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Frequency {
    hz: u64,
}

impl Frequency {
    pub const fn new(hz: u64) -> Self {
        Self { hz }
    }

    pub fn from_hz<T: AsPrimitive<u64>>(val: T) -> Self {
        Self { hz: val.as_() }
    }

    pub fn from_mhz<T: AsPrimitive<f64>>(val: T) -> Self {
        Self {
            hz: (val.as_() * ONE_MHZ as f64) as u64,
        }
    }
    pub fn from_ghz<T: AsPrimitive<f64>>(val: T) -> Self {
        Self {
            hz: (val.as_() * ONE_GHZ as f64) as u64,
        }
    }
    pub fn to_hz<T: NumCast>(&self) -> T {
        T::from(self.hz).unwrap()
    }

    pub fn to_mhz<T: NumCast>(&self) -> T {
        T::from(self.hz as f64 / ONE_MHZ as f64).unwrap()
    }

    pub fn to_ghz<T: NumCast>(&self) -> T {
        T::from(self.hz as f64 / ONE_GHZ as f64).unwrap()
    }
}

impl Add for Frequency {
    type Output = Frequency;

    fn add(self, rhs: Self) -> Self::Output {
        Self {
            hz: self.hz + rhs.hz,
        }
    }
}
impl AddAssign for Frequency {
    fn add_assign(&mut self, rhs: Self) {
        self.hz += rhs.hz;
    }
}

impl Sub for Frequency {
    type Output = Frequency;

    fn sub(self, rhs: Self) -> Self::Output {
        Self {
            hz: self.hz - rhs.hz,
        }
    }
}

impl SubAssign for Frequency {
    fn sub_assign(&mut self, rhs: Self) {
        self.hz -= rhs.hz;
    }
}

impl<N: NumCast> Mul<N> for Frequency {
    type Output = Frequency;

    fn mul(self, rhs: N) -> Self::Output {
        Self {
            hz: self.hz * rhs.to_u64().unwrap(),
        }
    }
}

impl Div<f32> for Frequency {
    type Output = Frequency;
    fn div(self, rhs: f32) -> Self::Output {
        Self {
            hz: ((self.hz as f32) / rhs) as u64,
        }
    }
}
impl Div<f64> for Frequency {
    type Output = Frequency;
    fn div(self, rhs: f64) -> Self::Output {
        Self {
            hz: ((self.hz as f64) / rhs) as u64,
        }
    }
}

impl<T: NumCast> ToFrequency for T {
    fn hz(self) -> Frequency {
        Frequency {
            hz: self.to_u64().unwrap(),
        }
    }

    fn mhz(self) -> Frequency {
        Frequency::from_mhz(self.to_f64().unwrap())
    }

    fn ghz(self) -> Frequency {
        Frequency::from_ghz(self.to_f64().unwrap())
    }
}

macro_rules! impl_div_num {
    ($T: ty) => {
        impl Div<$T> for Frequency {
            type Output = Frequency;
            fn div(self, rhs: $T) -> Self::Output {
                Self {
                    hz: self.hz / rhs as u64,
                }
            }
        }
    };
}

impl_div_num!(u8);
impl_div_num!(i8);
impl_div_num!(u16);
impl_div_num!(i16);
impl_div_num!(u32);
impl_div_num!(i32);
impl_div_num!(usize);
impl_div_num!(isize);
impl_div_num!(u64);
impl_div_num!(i64);

impl Div<Frequency> for Frequency {
    type Output = f64;

    fn div(self, rhs: Frequency) -> Self::Output {
        self.hz as f64 / rhs.hz as f64
    }
}

impl core::fmt::Debug for Frequency {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Frequency Hz ({})", self.hz)
    }
}
impl std::fmt::Display for Frequency {
    /// Formats the value using the given formatter.
    ///
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let mut value = self.hz as f64;
        let mut unit = " Hz";
        let mut precision = f.precision().unwrap_or(3);

        if self.hz >= ONE_GHZ {
            value /= ONE_GHZ as f64;
            unit = "GHz";
        } else if self.hz >= ONE_MHZ {
            value /= ONE_MHZ as f64;
            unit = "MHz";
        } else {
            precision = 0;
        }

        if let Some(width) = f.width() {
            if f.align().unwrap_or(Alignment::Left) == Alignment::Left {
                write!(f, "{:<width$.precision$}", value)?;
            } else {
                write!(f, "{:>width$.precision$}", value)?;
            }
        } else {
            f.write_fmt(format_args!("{:.precision$}", value))?;
        }

        f.write_str(unit)
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_freq() {
        let freq = Frequency::from_mhz(1.0);
        let a: f64 = freq.to_mhz();
        let b: i32 = freq.to_hz();

        assert_eq!(a, 1.0);
        assert_eq!(b, 1_000_000);

        println!("{}", freq);
    }

    #[test]
    fn test_freq_add() {
        let freq = Frequency::from_mhz(1.0);
        let freq2 = Frequency::from_mhz(2.0);
        let freq3 = freq + freq2;

        let a: f64 = freq3.to_mhz();

        assert_eq!(a, 3.0);
    }

    #[test]
    fn test_freq_div() {
        let freq = Frequency::from_mhz(1.0);
        let freq2 = Frequency::from_mhz(2.0);
        let p = freq / freq2;

        assert_eq!(p, 0.5);

        let freq = Frequency::from_mhz(3.0);
        let b = 2isize;
        let p = freq / b;
        let want = Frequency::from_mhz(1.5);

        assert_eq!(p, want);

        let freq = Frequency::from_mhz(1.0);
        let b = 0.5f32;
        let p = freq / b;
        let want = Frequency::from_mhz(2);

        assert_eq!(p, want);
    }
    #[test]
    fn test_print() {
        let freq = Frequency::from_mhz(12.3456789);

        assert_eq!(format!("{:<10.2}", freq), "12.35     MHz");

        let freq = Frequency::from_hz(12);

        println!("{:<10.2}", freq);
    }

    #[test]
    fn test_to_freq() {
        let freq = 1.hz();
        let freq2 = 1.mhz();
        let freq3 = 1.0.ghz();

        assert_eq!(freq.to_hz::<u64>(), 1);
        assert_eq!(freq2.to_hz::<u64>(), 1_000_000);
        assert_eq!(freq3.to_hz::<u64>(), 1_000_000_000);

        assert_eq!(1.1.mhz().to_hz::<u64>(), 1_100_000);
    }
}
